﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text.RegularExpressions;
using Bridge.Base.Utilities;
using Pathfinder.Base.Constructs;
using Pathfinder.Base.Utilities;

namespace Pathfinder.Base.Importer
{
    public static class OffenseStatblockImporter
    {
        /// <summary>
        /// Finds the Speeds in a Statblock.
        /// </summary>
        /// <param name="input">Statblock to search.</param>
        /// <returns>Dictionary of SpeedType found; null if nothing is found.</returns>
        /// <exception cref="ArgumentNullException">Thrown if input is null.</exception>
        /// <exception cref="ArgumentException">Thrown if input is empty.</exception>
        public static Dictionary<SpeedType, int> FindSpeeds(string input)
        {
            input.ThrowIfNullOrEmpty("input");

            return FindSpeeds(input, SpeedRegex);
        }

        /// <summary>
        /// Finds the Speeds in a Statblock.
        /// </summary>
        /// <param name="input">Statblock to search.</param>
        /// <param name="regex">Regex to use</param>
        /// <returns>Dictionary of SpeedType found; null if nothing is found.</returns>
        /// <exception cref="ArgumentNullException">Thrown if input or regex is null.</exception>
        /// <exception cref="ArgumentException">Thrown if input or regex is empty.</exception>
        public static Dictionary<SpeedType, int> FindSpeeds(string input, string regex)
        {
            input.ThrowIfNullOrEmpty("input");
            regex.ThrowIfNullOrEmpty("regex");

            Dictionary<SpeedType, int> speeds = new Dictionary<SpeedType,int>();
            foreach (SpeedType speedType in Enum.GetValues(typeof(SpeedType)))
            {
                string findRegex = string.Format(regex, speedType == SpeedType.Base ? "Speed" : speedType.ToString());
                string foundInfo = input.FindFirstMatch(findRegex);
                if (!string.IsNullOrEmpty(foundInfo))
                {
                    speeds.Add(speedType, int.Parse(foundInfo));
                }
            }

            return speeds;
        }

        /// <summary>
        /// Finds the Speeds in a Statblock.
        /// </summary>
        /// <param name="input">Statblock to search.</param>
        /// <param name="isMelee">If true, gather melee attacks; otherwise ranged.</param>
        /// <returns>Strings of attacks. found; null if nothing is found.</returns>
        /// <exception cref="ArgumentNullException">Thrown if input is null.</exception>
        /// <exception cref="ArgumentException">Thrown if input is empty.</exception>
        public static IEnumerable<string> FindAttacks(string input, bool isMelee)
        {
            input.ThrowIfNullOrEmpty("input");

            return FindAttacks(input, isMelee, AttackSplitRegex);
        }

        /// <summary>
        /// Finds the Speeds in a Statblock.
        /// </summary>
        /// <param name="input">Statblock to search.</param>
        /// <param name="isMelee">If true, gather melee attacks; otherwise ranged.</param>
        /// <param name="splitRegex">Regex to use to split the attacks.</param>
        /// <returns>Strings of attacks. found; null if nothing is found.</returns>
        /// <exception cref="ArgumentNullException">Thrown if input, splitRegex is null.</exception>
        /// <exception cref="ArgumentException">Thrown if input, splitRegex is empty.</exception>
        public static IEnumerable<string> FindAttacks(string input, bool isMelee, string splitRegex)
        {
            input.ThrowIfNullOrEmpty("input");
            splitRegex.ThrowIfNullOrEmpty("splitRegex");

            return FindAttacks(input, isMelee ? MeleeAttackRegex : RangedAttackRegex, splitRegex);
        }

        /// <summary>
        /// Finds the Speeds in a Statblock.
        /// </summary>
        /// <param name="input">Statblock to search.</param>
        /// <param name="regex">Regex to use</param>
        /// <param name="splitRegex">Regex to use to split the attacks.</param>
        /// <returns>Strings of attacks. found; null if nothing is found.</returns>
        /// <exception cref="ArgumentNullException">Thrown if input, splitRegex or regex is null.</exception>
        /// <exception cref="ArgumentException">Thrown if input, splitRegex or regex is empty.</exception>
        public static IEnumerable<string> FindAttacks(string input, string regex, string splitRegex)
        {
            input.ThrowIfNullOrEmpty("input");
            regex.ThrowIfNullOrEmpty("regex");
            splitRegex.ThrowIfNullOrEmpty("splitRegex");

            string attacks = input.FindFirstMatch(regex);
            if (attacks != null)
            {
                attacks = attacks.Replace("\r\n", " ");
                List<string> foundAttacks = new List<string>();
                foreach (string attack in attacks.FindAllMatches(splitRegex))
                {
                    foundAttacks.Add(attack);
                }
                return foundAttacks;
            }
            return null;
        }

        /// <summary>
        /// Finds the attack information from the provided string.
        /// </summary>
        /// <param name="input">Attack string to parse.</param>
        /// <param name="isRanged">If true, attack is ranged; otherwise Melee.</param>
        /// <param name="character">PathfinderCharacter to use for creating attacks.</param>
        /// <returns>Attack found.</returns>
        /// <exception cref="ArgumentNullException">Thrown if input or character is null.</exception>
        /// <exception cref="ArgumentException">Thrown if input is empty.</exception>
        public static Attack FindAttack(string input, bool isRanged, PathfinderCharacter character)
        {
            input.ThrowIfNullOrEmpty("input");
            if (character == null) throw new ArgumentNullException("character");
            
            string toHitBlock = input.FindLastMatch(@".+?(?=\()");
            if (toHitBlock == null) return null;
            string damageBlock = input.FindLastMatch(@"(?<=\().+?(?=\))");
            if (damageBlock == null) return null;

            string toHitPlus = toHitBlock.FindFirstMatch(@"((?<= \+)|(?<= \-)|(?<= –))\d+") ?? "+0";
            bool isPlus = Regex.IsMatch(toHitBlock, @" \+");
            string weapon = toHitBlock.FindFirstMatch(@".+?((?= \+)|(?= \-))") ?? string.Empty;
            bool isManafactured = Regex.IsMatch(toHitBlock, @"\d+/\+\d+");
            string numberAttacks = toHitBlock.FindFirstMatch(@"^\d+") ?? "1";

            string damageDice = damageBlock.FindFirstMatch(@"\d*?d\d+") ?? "0d1";
            string damageDiceAmount = damageDice.FindFirstMatch(@"\d+?(?=d)");
            string damageDiceFace = damageDice.FindFirstMatch(@"(?<=d)\d+");
            int damageActualDiceFace = 0;
            int damageActualDiceAmount = 1;
            int.TryParse(damageDiceFace, out damageActualDiceFace);
            if (!string.IsNullOrEmpty(damageDiceAmount))
            {
                int.TryParse(damageDiceAmount, out damageActualDiceAmount);
            }
            string damagePlus = damageBlock.FindFirstMatch(@"((?<=\+)|(?<=\-)|(?<=–))\d+") ?? "+0";
            bool damageIsPlus = Regex.IsMatch(damageBlock, @"\+");
            int actualDamagePlus = (int.Parse(damagePlus) * (damageIsPlus ? 1 : -1));
            int strRemove = 0;
            bool strBonus150 = false;
            int strBonus = character.TotalStats[StatType.Strength].GetBonus();
            if (Math.Floor(strBonus * 1.5) <= actualDamagePlus && strBonus >= 0)
            {
                strRemove = (int)Math.Floor(strBonus * 1.5);
                strBonus150 = true;
            }
            else
            {
                strRemove = strBonus;
                strBonus150 = false;
            }
            actualDamagePlus -= strRemove;

            Damage damage = new Damage()
            {
                DiceAmount = damageActualDiceAmount,
                DiceFace = damageActualDiceFace,
                FlatAmount = actualDamagePlus
            };

            string damageRange = damageBlock.FindFirstMatch(@"(?<=/)\d+") ?? "20";
            string damageMulti = damageBlock.FindFirstMatch(@"((?<=/×)|(?<=/X))\d+") ?? "2";

            string damagePlusBlock = damageBlock.FindFirstMatch(@"(?<=plus ).+");
            List<Damage> bonusDamages = new List<Damage>();
            if (!string.IsNullOrEmpty(damagePlusBlock))
            {
                StringCollection damagesPlusses = damagePlusBlock.FindAllMatches(@"((?<=^)|(?<=and )|(?<=\+ )|(?<=\+)).+?((?<=$)|(?<= and)|(?<= \+)|(?<=\+))");
                if (damagesPlusses != null)
                {
                    foreach (string damageString in damagesPlusses)
                    {
                        string damageDices = damageString.FindFirstMatch(@"\d*?d\d+");
                        string damageFlat = (string.IsNullOrEmpty(damageDices) ? damageString.FindFirstMatch(@"\d+") : null);
                        string damageName = string.Empty;
                        int bonusDamageDiceAmount = 0;
                        int bonusDamageDiceFace = 0;
                        int bonusDamageActualFlat = 0;
                        foreach (string stringPart in damageString.Split(' '))
                        {
                            if (!Regex.IsMatch(stringPart, @"\d"))
                            {
                                damageName += (damageName.Length > 0 ? " " : "") + stringPart;
                            }
                        }
                        if (!string.IsNullOrEmpty(damageDices))
                        {
                            string bonusDamageDiceAmountString = damageDices.FindFirstMatch(@"\d+?(?=d)");
                            string bonusDamageDiceFaceString = damageDices.FindFirstMatch(@"(?<=d)\d+");
                            if (!string.IsNullOrEmpty(bonusDamageDiceAmountString))
                            {
                                int.TryParse(bonusDamageDiceAmountString, out bonusDamageDiceAmount);
                            }
                            if (!string.IsNullOrEmpty(bonusDamageDiceFaceString))
                            {
                                int.TryParse(bonusDamageDiceFaceString, out bonusDamageDiceFace);
                            }
                        }
                        if (!string.IsNullOrEmpty(damageFlat))
                        {
                            int.TryParse(damageFlat, out bonusDamageActualFlat);
                        }
                        bonusDamages.Add(
                            new Damage()
                            {
                                DamageName = damageName,
                                FlatAmount = bonusDamageActualFlat,
                                MultipliedOnCrit = false,
                                AppliedOnlyOnCrit = false,
                                DiceAmount = bonusDamageDiceAmount,
                                DiceFace = bonusDamageDiceFace
                            });
                    }
                }
            }

            bool hasWeaponFinesse = character.HasFeat("Weapon Finesse");
            int dexBonus = character.TotalStats[StatType.Dexterity].GetBonus();

            int actualToHitPlus = (int.Parse(toHitPlus) * (isPlus ? 1 : -1)) - (hasWeaponFinesse ? dexBonus : strBonus) - character.BAB;

            Attack attack = new Attack(character);
            attack.Name = weapon;
            attack.IsNatural = !isManafactured;
            attack.NumberAttacks = (numberAttacks != null ? int.Parse(numberAttacks) : 1);
            attack.IsRanged = isRanged;
            attack.IsFinessable = hasWeaponFinesse;
            attack.HitBonus = actualToHitPlus;
            attack.BaseDamage = damage;
            attack.CritMultiplier = int.Parse(damageMulti);
            attack.CritRange = int.Parse(damageRange);
            attack.StrBonus150 = strBonus150;
            foreach (Damage bonusDamage in bonusDamages)
            {
                attack.AddDamage(bonusDamage);
            }

            return attack;
        }

        /// <summary>
        /// Finds the Special Attacks in a Statblock.
        /// </summary>
        /// <param name="input">Statblock to search.</param>
        /// <returns>Special attacks found; null if nothing is found.</returns>
        /// <exception cref="ArgumentNullException">Thrown if input is null.</exception>
        /// <exception cref="ArgumentException">Thrown if input is empty.</exception>
        public static string FindSpecialAttacks(string input)
        {
            input.ThrowIfNullOrEmpty("input");

            return FindSpecialAttacks(input, SpecialAttackRegex);
        }

        /// <summary>
        /// Finds the Special Attacks in a Statblock.
        /// </summary>
        /// <param name="input">Statblock to search.</param>
        /// <param name="regex">Regex to use</param>
        /// <returns>Special attacks found; null if nothing is found.</returns>
        /// <exception cref="ArgumentNullException">Thrown if input or regex is null.</exception>
        /// <exception cref="ArgumentException">Thrown if input or regex is empty.</exception>
        public static string FindSpecialAttacks(string input, string regex)
        {
            input.ThrowIfNullOrEmpty("input");
            regex.ThrowIfNullOrEmpty("regex");

            return input.FindFirstMatch(regex);
        }

        /// <summary>
        /// Finds the Spell-Like Abilities in a Statblock.
        /// </summary>
        /// <param name="input">Statblock to search.</param>
        /// <returns>Spell-Like Abilities found; null if nothing is found.</returns>
        /// <exception cref="ArgumentNullException">Thrown if input is null.</exception>
        /// <exception cref="ArgumentException">Thrown if input is empty.</exception>
        public static string FindSpellLikeAbilities(string input)
        {
            input.ThrowIfNullOrEmpty("input");

            return FindSpellLikeAbilities(input, SpellLikeAbilitiesRegex);
        }

        /// <summary>
        /// Finds the Spell-Like Abilities in a Statblock.
        /// </summary>
        /// <param name="input">Statblock to search.</param>
        /// <param name="regex">Regex to use</param>
        /// <returns>Spell-Like Abilities found; null if nothing is found.</returns>
        /// <exception cref="ArgumentNullException">Thrown if input or regex is null.</exception>
        /// <exception cref="ArgumentException">Thrown if input or regex is empty.</exception>
        public static string FindSpellLikeAbilities(string input, string regex)
        {
            input.ThrowIfNullOrEmpty("input");
            regex.ThrowIfNullOrEmpty("regex");

            return input.FindFirstMatch(regex);
        }

        /// <summary>
        /// Finds the FlyManuverability in a Statblock.
        /// </summary>
        /// <param name="input">Statblock to search.</param>
        /// <returns>FlyManuverability found; NA if nothing is found.</returns>
        /// <exception cref="ArgumentNullException">Thrown if input is null.</exception>
        /// <exception cref="ArgumentException">Thrown if input is empty.</exception>
        public static FlyManuverability FindFlyManuverability(string input)
        {
            input.ThrowIfNullOrEmpty("input");

            return FindFlyManuverability(input, FlyManuverabilityRegex);
        }

        /// <summary>
        /// Finds the FlyManuverability in a Statblock.
        /// </summary>
        /// <param name="input">Statblock to search.</param>
        /// <param name="regex">Regex to use</param>
        /// <returns>FlyManuverability found; NA if nothing is found.</returns>
        /// <exception cref="ArgumentNullException">Thrown if input or regex is null.</exception>
        /// <exception cref="ArgumentException">Thrown if input or regex is empty.</exception>
        public static FlyManuverability FindFlyManuverability(string input, string regex)
        {
            input.ThrowIfNullOrEmpty("input");
            regex.ThrowIfNullOrEmpty("regex");

            string flyManuverability = input.FindFirstMatch(regex);
            if (flyManuverability != null)
            {
                FlyManuverability flyManuv;

                if (flyManuverability.FindFirstMatch(@"(?<=\().+?(?=\))").TryGetEnumValue<FlyManuverability>(out flyManuv))
                {
                    return flyManuv;
                }
            }
            return FlyManuverability.NA;
        }

        /// <summary>
        /// Finds the Speed in a Statblock.
        /// </summary>
        /// <param name="input">Statblock to search.</param>
        /// <param name="regex">Regex to use</param>
        /// <returns>Speed found</returns>
        /// <exception cref="ArgumentNullException">Thrown if input is null.</exception>
        /// <exception cref="ArgumentException">Thrown if input is empty.</exception>
        public static int FindSpace(string input)
        {
            return FindSpace(input, SpaceRegex);
        }

        /// <summary>
        /// Finds the Speed in a Statblock.
        /// </summary>
        /// <param name="input">Statblock to search.</param>
        /// <param name="regex">Regex to use</param>
        /// <returns>Speed found</returns>
        /// <exception cref="ArgumentNullException">Thrown if input or regex is null.</exception>
        /// <exception cref="ArgumentException">Thrown if input or regex is empty.</exception>
        public static int FindSpace(string input, string regex)
        {
            input.ThrowIfNullOrEmpty("input");
            regex.ThrowIfNullOrEmpty("regex");

            return int.Parse(input.FindFirstMatch(regex) ?? "5");
        }

        /// <summary>
        /// Finds the Reach in a Statblock.
        /// </summary>
        /// <param name="input">Statblock to search.</param>
        /// <returns>Regex found</returns>
        /// <exception cref="ArgumentNullException">Thrown if input is null.</exception>
        /// <exception cref="ArgumentException">Thrown if input is empty.</exception>
        public static int FindReach(string input)
        {
            return FindReach(input, ReachRegex);
        }

        /// <summary>
        /// Finds the Reach in a Statblock.
        /// </summary>
        /// <param name="input">Statblock to search.</param>
        /// <param name="regex">Regex to use</param>
        /// <returns>Reach found</returns>
        /// <exception cref="ArgumentNullException">Thrown if input or regex is null.</exception>
        /// <exception cref="ArgumentException">Thrown if input or regex is empty.</exception>
        public static int FindReach(string input, string regex)
        {
            input.ThrowIfNullOrEmpty("input");
            regex.ThrowIfNullOrEmpty("regex");

            return int.Parse(input.FindFirstMatch(regex) ?? "5");
        }

        private const string SpeedRegex = @"(?<={0} )\d+";
        private const string AttackSplitRegex = @"((?<=^)|(?<=, )|(?<=or )|(?<=and )).+?\d+? *\(.+?\)";
        private const string MeleeAttackRegex = @"(?<=Melee ).+?((?=Spel)|(?=Rang)|(?=Spac)|(?=Tacti)|(?=Statis)|(?=Special))";
        private const string RangedAttackRegex = @"(?<=Ranged ).+?((?=Space)|(?=Tacti)|(?=Statistics))";
        private const string SpecialAttackRegex = @"(?<=Special Attacks ).+?((?=Spell)|(?=Stat))";
        private const string SpellLikeAbilitiesRegex = @"(?<=Spell\-Like Abilities ).+?((?=Base Stati)|(?=Sens)|(?=AC)|(?=Tacti)|(?=STATIS))";
        private const string FlyManuverabilityRegex = @"(?<=fly )\d.+?\(.+?\)";
        private const string SpaceRegex = @"(?<=Space )\d+";
        private const string ReachRegex = @"(?<=Reach )\d+";
    }
}
